
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id$
 */

package org.dellroad.sidekar.datastore;

import org.dellroad.sidekar.DetachedItemException;
import org.dellroad.sidekar.Entity;
import org.dellroad.sidekar.Index;
import org.dellroad.sidekar.ListField;
import org.dellroad.sidekar.MapField;
import org.dellroad.sidekar.SetField;
import org.dellroad.sidekar.SimpleField;
import org.dellroad.sidekar.query.JoinType;
import org.dellroad.sidekar.query.Query;
import org.dellroad.sidekar.query.QueryList;
import org.dellroad.sidekar.query.QueryMap;
import org.dellroad.sidekar.query.QuerySet;
import org.dellroad.sidekar.schema.change.EntityAbstractnessChange;
import org.dellroad.sidekar.schema.change.RemoveEntityChange;
import org.dellroad.sidekar.schema.change.SchemaChange;
import org.dellroad.sidekar.schema.change.SchemaMutator;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.sidekar.util.SidekarException;

/**
 * Sidekar datastore interface.
 *
 * <p>
 * Sidekar {@link Datastore} implementations provide the underlying persistent data storage for Sidekar.
 * Each instance represents some specific data storage structured according to a well-defined schema.
 * </p>
 *
 * <p>
 * Each instance has some datastore-specific notion of an underlying database to which it provides access.
 * This could be a traditional database, a flat file, an in-memory representation, etc.
 * Instances always "know" their configured schema, which is represented by a {@link SchemaModel} object.
 * When open, a datastore can be relied upon to access the entities, fields, and indexes defined in that schema.
 * In addition to regular data access, instances are capable of applying schema changes given to it by the Sidekar core.
 * </p>
 *
 * <p>
 * A never-before used datastore has an empty schema containing zero entities. The Sidekar core
 * will automatically build the schema using invocations of {@link #schemaChange}.
 * </p>
 *
 * <h3>Datastore Methods</h3>
 *
 * <p>
 * Datastore instances can be in one of two states, open or closed. Newly created instances are in the closed state.
 * The only methods that may be invoked in the closed state are {@link #datastoreOpen} and {@link #datastoreClose}
 * (which does nothing in the closed state). An instance that has been opened and then closed may be subsequently
 * re-opened by another call to {@link #datastoreOpen}. In the open state, all methods except {@link #datastoreOpen}
 * may be invoked.
 * </p>
 *
 * <p>
 * The remaining methods are divided into two types: methods that simply access data, and methods that access the schema.
 * The schema methods are {@link #schemaRetrieve} and {@link #schemaChange}; all others are data access methods.
 * </p>
 *
 * <h3>Threads and Locking</h3>
 *
 * <p>
 * {@link Datastore} instances must ensure that while any one thread is executing {@link #datastoreOpen}, {@link #datastoreClose},
 * or {@link #schemaChange}, no other thread may be executing within any of the methods in this interface (this can be implemented
 * using a simple read/write lock). Beyond this requirement, instances must support full multi-threaded operation.
 * </p>
 */
public interface Datastore extends SchemaMutator {

    /**
     * Open this instance.
     *
     * <p>
     * Threading: during the execution of this method, this instance must ensure that no other threads are accessing it.
     * </p>
     *
     * @throws IllegalStateException if this datastore is not in the closed state
     * @throws SidekarException if an error occurs
     */
    void datastoreOpen();

    /**
     * Close this instance.
     *
     * <p>
     * This method should eventually be invoked for all datastore instances not in the closed state, even those that have
     * encountered an unrecoverable error, to ensure all resources are released.
     * </p>
     *
     * <p>
     * If already in the closed state, nothing happens.
     * </p>
     *
     * <p>
     * Threading: during the execution of this method, this instance must ensure that no other threads are accessing it.
     * </p>
     *
     * @throws SidekarException if an error occurs
     */
    void datastoreClose();

    /**
     * Retrieve the datastore's currently configured schema.
     *
     * <p>
     * On a brand-new datastore, this method should return an empty schema (i.e., containing no entities).
     * </p>
     *
     * @return the schema that the datastore is currently configured to support
     * @throws IllegalStateException if this datastore is not open
     * @throws SidekarException if an error occurs
     */
    SchemaModel schemaRetrieve();

    /**
     * Modify the datastore's currently configured schema. The modification should be applied atomically (i.e., all or none).
     *
     * <p>
     * The {@code change} will have already had basic validation applied with respect to the currently configured
     * schema (as returned by {@link #schemaRetrieve}. For example, {@code change} won't refer to a non-existent entity
     * or contain illegal field names.
     * </p>
     *
     * <p>
     * After this method returns, a call to {@link #schemaRetrieve} must return an updated {@link SchemaModel}
     * reflecting the change just applied.
     * </p>
     *
     * <p>
     * This method is responsible for verifying the following:
     * <ul>
     * <li>For {@link EntityAbstractnessChange}, when going from non-abstract to abstract, there must not exist
     *  any exact instances of the entity being changed.</li>
     * <li>For {@link RemoveEntityChange}, there must not exist any references to the entity from any other entities.
     *  It suffices to only check exact references because the entity being removed is guaranteed to be a leaf entity.
     *  In addition, the entity is guaranteed to contain zero fields or indexes.</li>
     * </ul>
     * </p>
     *
     * <p>
     * Threading: during the execution of this method, this instance must ensure that no other threads are accessing it.
     * </p>
     *
     * @param change the change to apply to the schema
     * @throws IllegalStateException if this datastore is not open
     * @throws IllegalStateException if the underlying datastore is not initialized
     * @throws SidekarException if an error occurs
     */
    @Override
    void schemaChange(SchemaChange change);

    /**
     * Create a new instance of an entity.
     *
     * @param entity the new instance's type
     * @return a new and unique ID for the instance; must not be equal to zero
     * @throws IllegalStateException if this datastore is not open
     * @throws SidekarException if an error occurs
     */
    long instanceCreate(Entity<?> entity);

    /**
     * (Re)attach an instance of an entity. If the instance is already attached, this should do nothing.
     *
     * @param entity the new instance's type
     * @param id the ID of the instance, never zero
     * @throws IllegalArgumentException if {@code id} is zero
     * @throws IllegalStateException if this datastore is not open
     * @throws SidekarException if an error occurs
     */
    void instanceAttach(Entity<?> entity, long id);

    /**
     * Delete an instance of an entity. If the instance has already been deleted previously (or does not exist),
     * this method should do nothing and return normally.
     *
     * <p>
     * The caller is not required to empty any collections fields associated with the instance before invoking
     * this method. Therefore, depending on the implementation, those additional implied steps may be necessary.
     * </p>
     *
     * @param entity the type of the instance
     * @param id the ID of the instance, never zero
     * @throws IllegalArgumentException if {@code id} is zero
     * @throws IllegalStateException if this datastore is not open
     * @throws SidekarException if an error occurs
     */
    void instanceDetach(Entity<?> entity, long id);

    /**
     * Determine whether an instance exists in the datastore.
     *
     * @param entity the type of the instance
     * @param id the ID of the instance, never zero
     * @throws IllegalArgumentException if {@code id} is zero
     * @throws IllegalStateException if this datastore is not open
     * @throws SidekarException if an error occurs
     */
    boolean instanceIsAttached(Entity<?> entity, long id);

    /**
     * Access all instances of a given entity.
     *
     * @param entity the type of the instance
     * @param exact whether to match only exact instances of {@code entity} or to match child entities as well
     * @throws IllegalStateException if this datastore is not open
     * @throws SidekarException if an error occurs
     */
    <E> QuerySet<E> entityAccess(Entity<E> entity, boolean exact);

    /**
     * Read an instance field.
     *
     * @param entity the type of the instance
     * @param id the ID of the instance
     * @param field the field of the entity
     * @throws IllegalStateException if this datastore is not open
     * @throws DetachedItemException if the item is no longer attached to the underlying datastore
     * @throws SidekarException if an error occurs
     */
    <T> T simpleFieldRead(Entity<?> entity, long id, SimpleField<T> field);

    /**
     * Update an instance field.
     *
     * @param entity the type of the instance
     * @param id the ID of the instance
     * @param field the field of the entity
     * @param value new value
     * @throws IllegalStateException if this datastore is not open
     * @throws DetachedItemException if the item is no longer attached to the underlying datastore
     * @throws SidekarException if an error occurs
     */
    <T> void simpleFieldWrite(Entity<?> entity, long id, SimpleField<T> field, T value);

    /**
     * Access a set field.
     *
     * @param entity the type of the instance
     * @param id the ID of the instance
     * @param field the field of the entity
     * @throws IllegalStateException if this datastore is not open
     * @throws DetachedItemException if the item is no longer attached to the underlying datastore
     * @throws SidekarException if an error occurs
     */
    <E> QuerySet<E> setFieldAccess(Entity<?> entity, long id, SetField<E> field);

    /**
     * Access a map field.
     *
     * @param entity the type of the instance
     * @param id the ID of the instance
     * @param field the field of the entity
     * @throws IllegalStateException if this datastore is not open
     * @throws DetachedItemException if the item is no longer attached to the underlying datastore
     * @throws SidekarException if an error occurs
     */
    <K, V> QueryMap<K, V> mapFieldAccess(Entity<?> entity, long id, MapField<K, V> field);

    /**
     * Access a list field.
     *
     * @param entity the type of the instance
     * @param id the ID of the instance
     * @param field the field of the entity
     * @throws IllegalStateException if this datastore is not open
     * @throws DetachedItemException if the item is no longer attached to the underlying datastore
     * @throws SidekarException if an error occurs
     */
    <E> QueryList<E> listFieldAccess(Entity<?> entity, long id, ListField<E> field);

    /**
     * Access an index.
     *
     * <p>
     * Whether an already-opened {@link Query} can include new items that only became members of the index after
     * it was opened is a matter up to the datastore implementation.
     * </p>
     *
     * @param entity the type of the instance
     * @param index the index on the entity
     * @param values values for zero or more of the index's fields
     * @throws IllegalStateException if this datastore is not open
     * @throws DetachedItemException if the item is no longer attached to the underlying datastore
     * @throws SidekarException if an error occurs
     */
    <E> QuerySet<E> indexAccess(Entity<?> entity, Index<E> index, Object... values);
    
    /**
     * Join two query sets.
     *
     * <p>
     * In order to join two queries, the queries must be <i>join compatible</i>, which means their elements must
     * have compatible types and iterate in the same sort order. TODO: define "compatible types".
     * </p>
     *
     * @param left left-side query
     * @param right right-side query
     * @param joinType type of join
     * @param <R> the type of element in the right query
     * @param <L> the type of element in the left query
     * @param <E> the type of element in the joined query
     * @return a read-only query set containing the joined elements
     * @throws IllegalArgumentException if {@code right} or {@code left} is null
     * @throws IllegalArgumentException if {@code joinType} is null
     * @throws IllegalArgumentException if {@code right} and {@code right} are not join-compatible
     * @throws IllegalStateException if this instance is not in the running state
     * @throws SidekarException if an error occurs
     */
    <E, R extends E, L extends E> QuerySet<E> join(QuerySet<L> left, QuerySet<R> right, JoinType joinType);
}

